AgentDeck · android · ui/eink

E-ink redesign — applied to the codebase

The design canvas (E-ink Dashboard.html) is the spec. This page is the real-code reflection: every decision from the canvas mapped to a concrete file in android/app/src/main/kotlin/dev/agentdeck/, ready to drop in.

Files changed

EDIT
ui/eink/EinkRefreshZone.kt
New RefreshMode.FULL_ONCE variant — semantic "one clean GC16 flash on appearance, then stay quiet". Maps to the same vendor call as FULL but separates intent.
NEW
ui/eink/EinkZonePolicy.kt
Single-source-of-truth refresh policy matrix. Zone enum captures mode + debounce per zone so call sites stop hardcoding waveform constants.
NEW
ui/eink/EinkScenarios.kt
Five canonical multi-session fixtures (1×–5× sessions, mixed agent types) for @Preview and screenshot tests.
EDIT
terrarium/renderer/EinkRenderer.kt
Kobo / Tolino / KOReader fallback added to EinkRefreshHelper.requestFullRefresh via SystemProperties.set("sys.eink.update", "GC16"). Falls through to view.invalidate() on unsupported devices.
EDIT
ui/screen/EinkMonitorScreen.kt
ATTENTION extracted as its own EinkRefreshZone with FULL_ONCE policy. All zone call sites (5 in landscape, 3 in portrait) now read mode + debounce from Zone.CHROME / ATTENTION / CONTEXT_FAST / STATUS_SLOW / TIMELINE instead of inline literals.

The refresh matrix — one source, six zones

Before: every zone hardcoded its mode/debounce inline at the call site (RefreshMode.A2, 200). Six zones × landscape + portrait = 11 duplicates that drifted over time. After: every call site reads from Zone.

ZoneModeDebounceTrigger
CHROME A2 200 ms session list / agent states
TERRARIUM animatedper-framecreature animation loop
ATTENTION FULL_ONCE80 ms awaiting state appears
CONTEXT_FAST A2 200 ms tool / option changes
STATUS_SLOW DU 2000 ms usage / health / models
TIMELINE A2 300 ms append-only entry count

Why ATTENTION moves to its own zone

In the old code the featuredAttention != null branch lived inside the Context+Status EinkRefreshZone on RefreshMode.A2. Two problems:

  1. The prompt panel inherited residual A2 ghosting from neighbouring tool / gauge updates.
  2. Onset of AWAITING_PERMISSION never forced a clean GC16 flash — the prompt just appeared in whatever waveform mode the zone happened to be in.
// after — EinkMonitorScreen.kt
if (featuredAttention != null) {
    EinkRefreshZone(
        mode        = Zone.ATTENTION.mode,        // FULL_ONCE
        debounceMs  = Zone.ATTENTION.debounceMs,  // 80 ms
        triggerKey  = featuredAttention,
    ) { /* permission / option panel */ }
} else if (hasContext || showStatusPanel) {
    // CONTEXT_FAST / STATUS_SLOW — never sees the prompt
}

Driver port — what landed

The proposal called for a brand-new EpdRefreshController interface. Reading EinkRefreshHelper showed the helper IS the port — Rockchip + Onyx already covered. Minimal change instead: add a third fallback for Kobo / Tolino, document the chain.

fun requestFullRefresh(view: View) {
    // 1. Rockchip RK35xx (Crema S, Xiaomi Reader)
    if (tryRockchipRefresh(view, RK_EPD_FULL_GC16, sendFullFrame = !einkColorEnabled)) return

    // 2. Onyx Boox (Qualcomm)
    try { onyxClass.getMethod("requestScreenUpdate", View::class.java).invoke(null, view); return }
    catch (_: Exception) {}

    +   // 3. Kobo / Tolino / KOReader — system property bridge
    +   if (tryKoboRefresh(view, koboMode = "GC16")) return

    // 4. Fallback: standard invalidate
    view.invalidate()
}

Five canonical multi-agent scenarios

The design canvas illustrated five session topologies; they now exist as EinkScenarios fixtures consumable from @Preview parameters and screenshot tests.

01 · TYPICAL
typical
Claude×2 Codex OpenClaw
Everyday workload — primary Claude processing, peers idle.
02 · CLAUDE-HEAVY
claudeHeavy
Claude×3 Codex OpenCode
Three Claude sessions concurrent — Opus, Sonnet, Haiku.
03 · AWAIT-MIXED
claudeAwaiting
Claude AWAIT Claude Codex OpenClaw
One Claude blocked on permission while peer keeps processing — the FULL_ONCE case.
04 · CODEX-FOCUSED
codexFocused
Codex×2 OpenCode Claude
Claude not the primary — chrome must not assume Claude-first ordering.
05 · SOLO
soloOpenCode
OpenCode
Single sparse session — collapsed chrome, terrarium hosts one creature.

Migration order — three commits

  1. RISK · LOW · ADDITIVE
    Add primitives, no UI change
  2. RISK · LOW · MECHANICAL
    Wire Zone.* into EinkMonitorScreen
  3. RISK · MEDIUM · USER-VISIBLE
    Extract ATTENTION zone

Risk summary

PathKindRisk
ui/eink/EinkRefreshZone.ktadd enum variantLOW
ui/eink/EinkZonePolicy.ktnew fileLOW
ui/eink/EinkScenarios.ktnew file (test only)LOW
terrarium/renderer/EinkRenderer.ktadd fallback driverLOW
ui/screen/EinkMonitorScreen.ktextract ATTENTION zoneMEDIUM

Drop-in files live under patches/ in this project. patches/new/ contains the three new files; the other three are full pre-edited copies of the originals so you can diff them against your working tree.